// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package guardduty

import (
	"context"
	"errors"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/guardduty"
	awstypes "github.com/aws/aws-sdk-go-v2/service/guardduty/types"
	"github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
	"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
	"github.com/hashicorp/terraform-plugin-framework/resource"
	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
	"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
	"github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier"
	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
	"github.com/hashicorp/terraform-plugin-framework/types"
	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
	"github.com/hashicorp/terraform-provider-aws/internal/create"
	"github.com/hashicorp/terraform-provider-aws/internal/framework"
	"github.com/hashicorp/terraform-provider-aws/internal/framework/flex"
	fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types"
	tftags "github.com/hashicorp/terraform-provider-aws/internal/tags"
	"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
	"github.com/hashicorp/terraform-provider-aws/names"
)

const iamPropagationTimeout = 2 * time.Minute

// @FrameworkResource("aws_guardduty_malware_protection_plan", name="Malware Protection Plan")
// @Tags(identifierAttribute="arn")
// @Testing(preCheck="testAccPreCheck")
// @Testing(existsType="github.com/aws/aws-sdk-go-v2/service/guardduty;guardduty.GetMalwareProtectionPlanOutput")
func newMalwareProtectionPlanResource(_ context.Context) (resource.ResourceWithConfigure, error) {
	r := &malwareProtectionPlanResource{}

	return r, nil
}

const (
	ResNameMalwareProtectionPlan = "Malware Protection Plan"
)

type malwareProtectionPlanResource struct {
	framework.ResourceWithModel[malwareProtectionPlanResourceModel]
	framework.WithImportByID
}

func (r *malwareProtectionPlanResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			names.AttrActions: schema.ListAttribute{ // proto5 Optional+Computed nested block.
				CustomType: fwtypes.NewListNestedObjectTypeOf[actionsModel](ctx),
				Optional:   true,
				Computed:   true,
				PlanModifiers: []planmodifier.List{
					listplanmodifier.UseStateForUnknown(),
				},
				Validators: []validator.List{
					listvalidator.SizeAtMost(1),
				},
				ElementType: types.ObjectType{
					AttrTypes: fwtypes.AttributeTypesMust[actionsModel](ctx),
				},
			},
			names.AttrARN: framework.ARNAttributeComputedOnly(),
			names.AttrCreatedAt: schema.StringAttribute{
				CustomType: timetypes.RFC3339Type{},
				Computed:   true,
				PlanModifiers: []planmodifier.String{
					stringplanmodifier.UseStateForUnknown(),
				},
			},
			names.AttrID: framework.IDAttribute(),
			names.AttrRole: schema.StringAttribute{
				CustomType: fwtypes.ARNType,
				Required:   true,
			},
			names.AttrStatus: schema.StringAttribute{
				Computed: true,
			},
			names.AttrTags:    tftags.TagsAttribute(),
			names.AttrTagsAll: tftags.TagsAttributeComputedOnly(),
		},
		Blocks: map[string]schema.Block{
			"protected_resource": schema.ListNestedBlock{
				CustomType: fwtypes.NewListNestedObjectTypeOf[protectedResourceModel](ctx),
				Validators: []validator.List{
					listvalidator.IsRequired(),
					listvalidator.SizeAtLeast(1),
					listvalidator.SizeAtMost(1),
				},
				NestedObject: schema.NestedBlockObject{
					Blocks: map[string]schema.Block{
						names.AttrS3Bucket: schema.ListNestedBlock{
							CustomType: fwtypes.NewListNestedObjectTypeOf[s3BucketModel](ctx),
							Validators: []validator.List{
								listvalidator.IsRequired(),
								listvalidator.SizeAtLeast(1),
								listvalidator.SizeAtMost(1),
							},
							NestedObject: schema.NestedBlockObject{
								Attributes: map[string]schema.Attribute{
									names.AttrBucketName: schema.StringAttribute{
										Required: true,
										PlanModifiers: []planmodifier.String{
											stringplanmodifier.RequiresReplace(),
										},
									},
									"object_prefixes": schema.SetAttribute{
										CustomType:  fwtypes.SetOfStringType,
										ElementType: types.StringType,
										Optional:    true,
										Computed:    true,
										PlanModifiers: []planmodifier.Set{
											setplanmodifier.UseStateForUnknown(),
										},
									},
								},
							},
						},
					},
				},
			},
		},
	}
}

func (r *malwareProtectionPlanResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	conn := r.Meta().GuardDutyClient(ctx)
	var plan malwareProtectionPlanResourceModel

	resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)

	input := &guardduty.CreateMalwareProtectionPlanInput{}
	resp.Diagnostics.Append(flex.Expand(ctx, plan, input)...)

	if resp.Diagnostics.HasError() {
		return
	}

	input.Tags = getTagsIn(ctx)

	var out *guardduty.CreateMalwareProtectionPlanOutput

	err := tfresource.Retry(ctx, iamPropagationTimeout, func(ctx context.Context) *tfresource.RetryError {
		var err error
		out, err = conn.CreateMalwareProtectionPlan(ctx, input)
		if err != nil {
			var nfe *awstypes.ResourceNotFoundException
			var bre *awstypes.BadRequestException // Error returned due to IAM eventual consistency
			if errors.As(err, &nfe) {
				return tfresource.RetryableError(err)
			} else if errors.As(err, &bre) {
				return tfresource.RetryableError(err)
			}
			return tfresource.NonRetryableError(err)
		}

		return nil
	})

	if err != nil {
		resp.Diagnostics.AddError(
			create.ProblemStandardMessage(names.GuardDuty, create.ErrActionCreating, ResNameMalwareProtectionPlan, "malware protection", nil),
			err.Error(),
		)
		return
	}

	state := plan
	state.ID = flex.StringToFramework(ctx, out.MalwareProtectionPlanId)

	// Read after create to get computed attributes omitted from the create response
	readOut, err := findMalwareProtectionPlanByID(ctx, conn, state.ID.ValueString())
	if err != nil {
		resp.Diagnostics.AddError(
			create.ProblemStandardMessage(names.SSOAdmin, create.ErrActionCreating, ResNameMalwareProtectionPlan, plan.ID.String(), err),
			err.Error(),
		)
		return
	}
	resp.Diagnostics.Append(flex.Flatten(ctx, readOut, &state)...)

	if resp.Diagnostics.HasError() {
		return
	}

	resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

func (r *malwareProtectionPlanResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
	conn := r.Meta().GuardDutyClient(ctx)
	var state malwareProtectionPlanResourceModel

	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)

	if resp.Diagnostics.HasError() {
		return
	}

	out, err := findMalwareProtectionPlanByID(ctx, conn, state.ID.ValueString())

	if tfresource.NotFound(err) {
		resp.State.RemoveResource(ctx)
		return
	}

	if err != nil {
		resp.Diagnostics.AddError(
			create.ProblemStandardMessage(names.GuardDuty, create.ErrActionSetting, ResNameMalwareProtectionPlan, state.ID.ValueString(), err),
			err.Error(),
		)
		return
	}

	resp.Diagnostics.Append(flex.Flatten(ctx, out, &state)...)

	if resp.Diagnostics.HasError() {
		return
	}

	resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

func (r *malwareProtectionPlanResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
	conn := r.Meta().GuardDutyClient(ctx)
	var state, plan malwareProtectionPlanResourceModel

	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)

	if resp.Diagnostics.HasError() {
		return
	}

	resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)

	if resp.Diagnostics.HasError() {
		return
	}

	if malwareProtectionPlanHasChanges(ctx, plan, state) {
		input := &guardduty.UpdateMalwareProtectionPlanInput{}

		resp.Diagnostics.Append(flex.Expand(ctx, plan, input)...)

		if resp.Diagnostics.HasError() {
			return
		}

		input.MalwareProtectionPlanId = flex.StringFromFramework(ctx, state.ID)

		_, err := conn.UpdateMalwareProtectionPlan(ctx, input)

		if err != nil {
			resp.Diagnostics.AddError(
				create.ProblemStandardMessage(names.GuardDuty, create.ErrActionUpdating, ResNameMalwareProtectionPlan, state.ID.ValueString(), err),
				err.Error(),
			)
			return
		}
	}

	out, err := findMalwareProtectionPlanByID(ctx, conn, state.ID.ValueString())

	if err != nil {
		resp.Diagnostics.AddError(
			create.ProblemStandardMessage(names.GuardDuty, create.ErrActionUpdating, ResNameMalwareProtectionPlan, state.ID.ValueString(), err),
			err.Error(),
		)
		return
	}

	resp.Diagnostics.Append(flex.Flatten(ctx, out, &plan)...)

	if resp.Diagnostics.HasError() {
		return
	}

	resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

func (r *malwareProtectionPlanResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
	conn := r.Meta().GuardDutyClient(ctx)

	var state malwareProtectionPlanResourceModel
	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
	if resp.Diagnostics.HasError() {
		return
	}

	_, err := conn.DeleteMalwareProtectionPlan(ctx, &guardduty.DeleteMalwareProtectionPlanInput{
		MalwareProtectionPlanId: state.ID.ValueStringPointer(),
	})
	if err != nil {
		var nfe *awstypes.ResourceNotFoundException
		if errors.As(err, &nfe) {
			return
		}
		resp.Diagnostics.AddError(
			create.ProblemStandardMessage(names.GuardDuty, create.ErrActionDeleting, ResNameMalwareProtectionPlan, state.ID.String(), nil),
			err.Error(),
		)
	}
}

type malwareProtectionPlanResourceModel struct {
	framework.WithRegionModel
	Actions           fwtypes.ListNestedObjectValueOf[actionsModel]           `tfsdk:"actions"`
	ARN               types.String                                            `tfsdk:"arn"`
	CreatedAt         timetypes.RFC3339                                       `tfsdk:"created_at"`
	ID                types.String                                            `tfsdk:"id"`
	ProtectedResource fwtypes.ListNestedObjectValueOf[protectedResourceModel] `tfsdk:"protected_resource"`
	Role              fwtypes.ARN                                             `tfsdk:"role"`
	Status            types.String                                            `tfsdk:"status"`
	Tags              tftags.Map                                              `tfsdk:"tags"`
	TagsAll           tftags.Map                                              `tfsdk:"tags_all"`
}

type actionsModel struct {
	Tagging fwtypes.ListNestedObjectValueOf[taggingModel] `tfsdk:"tagging"`
}

type taggingModel struct {
	Status fwtypes.StringEnum[awstypes.MalwareProtectionPlanTaggingActionStatus] `tfsdk:"status"`
}

type protectedResourceModel struct {
	S3Bucket fwtypes.ListNestedObjectValueOf[s3BucketModel] `tfsdk:"s3_bucket"`
}

type s3BucketModel struct {
	BucketName     types.String                     `tfsdk:"bucket_name"`
	ObjectPrefixes fwtypes.SetValueOf[types.String] `tfsdk:"object_prefixes"`
}

func malwareProtectionPlanHasChanges(_ context.Context, plan, state malwareProtectionPlanResourceModel) bool {
	return !plan.Actions.Equal(state.Actions) ||
		!plan.ProtectedResource.Equal(state.ProtectedResource) ||
		!plan.Role.Equal(state.Role)
}

func findMalwareProtectionPlanByID(ctx context.Context, conn *guardduty.Client, id string) (*guardduty.GetMalwareProtectionPlanOutput, error) {
	input := &guardduty.GetMalwareProtectionPlanInput{
		MalwareProtectionPlanId: aws.String(id),
	}

	return findMalwareProtectionPlan(ctx, conn, input)
}

func findMalwareProtectionPlan(ctx context.Context, conn *guardduty.Client, input *guardduty.GetMalwareProtectionPlanInput) (*guardduty.GetMalwareProtectionPlanOutput, error) {
	result, err := conn.GetMalwareProtectionPlan(ctx, input)

	if err != nil {
		var nfe *awstypes.ResourceNotFoundException
		if errors.As(err, &nfe) {
			return nil, &retry.NotFoundError{
				LastError:   err,
				LastRequest: input,
			}
		}

		return nil, err
	}

	if result == nil {
		return nil, tfresource.NewEmptyResultError(input)
	}

	return result, nil
}
